from IPython.display import display
On considère des vecteurs de features de dimension $n$
$$\mathbf{x} = (x₁, …, x_n)$$Un vecteur de poids de dimension $n$
$$\mathbf{w} = (w₁, …, w_n)$$et un biais $b$ scalaire (un nombre quoi).
Pour réaliser une classification on considère le nombre $z$ (on parle parfois de logit)
$$z=w₁×x₁ + … + w_n×x_n + b = \sum_iw_ix_i + b$$Ce qu'on note aussi
$$z = \mathbf{w}⋅\mathbf{x}+b$$$\mathbf{w}⋅\mathbf{x}$ se lit « w scalaire x », on parle de produit scalaire en français et de inner product en anglais.
(ou pour les mathématicien⋅ne⋅s acharné⋅e⋅s $z = \langle w\ |\ x \rangle + b$)
Quelle que soit la façon dont on le note, on affectera à $\mathbf{x}$ la classe $0$ si $z < 0$ et la classe $1$ sinon.
Écrire une fonction qui prend en entrée un vecteur de features et un vecteur de poids sous forme de
tableaux numpy $x$ et $w$ de dimensions (n,) et un biais $b$ sous forme d'un tableau numpy de
dimensions (1,) et renvoie $z=\sum_iw_ix_i + b$.
import numpy as np
def affine_combination(x, w, b):
pass # À vous de jouer !
affine_combination(
np.array([2, 0, 2, 1]),
np.array([-0.2, 999.1, 0.5, 2]),
np.array([1]),
)
def affine_combination(x, w, b):
res = np.zeros(1)
for wi, xi in zip(w, x):
res += wi*xi
res += b
return res
affine_combination(
np.array([2, 0, 2, 1]),
np.array([-0.2, 999.1, 0.5, 2]),
np.array([1]),
)
array([3.6])
def affine_combination(x, w, b):
return np.inner(w, x) + b
affine_combination(
np.array([2, 0, 2, 1]),
np.array([-0.2, 999.1, 0.5, 2]),
np.array([1]),
)
array([3.6])
import re
def poor_mans_tokenizer_and_normalizer(s):
return [w.lower() for w in re.split(r"\s|\W", s.strip()) if w and all(c.isalpha() for c in w)]
def read_vader(vader_path):
res = dict()
with open(vader_path) as in_stream:
for row in in_stream:
word, polarity, *_ = row.lstrip().split("\t", maxsplit=2)
is_positive = float(polarity) > 0
res[word] = is_positive
return res
def featurize(text, lexicon):
words = poor_mans_tokenizer_and_normalizer(text)
features = np.empty(2)
features[0] = sum(1 for w in words if lexicon.get(w, False))
features[1] = sum(1 for w in words if not lexicon.get(w, True))
return features
lexicon = read_vader("../../data/vader_lexicon.txt")
doc = "I came in in the middle of this film so I had no idea about any credits or even its title till I looked it up here, where I see that it has received a mixed reception by your commentators. I'm on the positive side regarding this film but one thing really caught my attention as I watched: the beautiful and sensitive score written in a Coplandesque Americana style. My surprise was great when I discovered the score to have been written by none other than John Williams himself. True he has written sensitive and poignant scores such as Schindler's List but one usually associates his name with such bombasticities as Star Wars. But in my opinion what Williams has written for this movie surpasses anything I've ever heard of his for tenderness, sensitivity and beauty, fully in keeping with the tender and lovely plot of the movie. And another recent score of his, for Catch Me if You Can, shows still more wit and sophistication. As to Stanley and Iris, I like education movies like How Green was my Valley and Konrack, that one with John Voigt and his young African American charges in South Carolina, and Danny deVito's Renaissance Man, etc. They tell a necessary story of intellectual and spiritual awakening, a story which can't be told often enough. This one is an excellent addition to that genre."
doc_features = featurize(doc, lexicon)
doc_features
array([13., 3.])
2. Appliquer la fonction précédente sur le mini-corpus IMDB
Commençons par l'extraire
%%bash
cd ../../local
tar -xzf ../data/imdb_smol.tar.gz
ls -lah imdb_smol
total 32K drwxr-xr-x 4 runner docker 4.0K Dec 4 2018 . drwxr-xr-x 4 runner docker 4.0K Oct 27 10:34 .. drwxr-xr-x 2 runner docker 12K Dec 4 2018 neg drwxr-xr-x 2 runner docker 12K Dec 4 2018 pos
tar: Ignoring unknown extended header keyword 'SCHILY.fflags' tar: Ignoring unknown extended header keyword 'SCHILY.fflags' tar: Ignoring unknown extended header keyword 'SCHILY.fflags'
Maintenant on parcourt le dossier pour construire nos représentations
from collections import defaultdict
import pathlib # Manipuler des chemins et des fichiers agréablement
def featurize_dir(corpus_root, lexicon):
corpus_root = pathlib.Path(corpus_root)
res = defaultdict(list)
for clss in corpus_root.iterdir():
# On peut aussi utiliser une compréhension de liste et avoir un dict pas default
for doc in clss.iterdir():
# `stem` et `read_text` c'est de la magie de `pathlib`, check it out
res[clss.stem].append(featurize(doc.read_text(), lexicon))
return res
# On réutilise le lexique précédent
imdb_features = featurize_dir("../../local/imdb_smol", lexicon)
imdb_features
defaultdict(list,
{'pos': [array([8., 7.]),
array([5., 0.]),
array([16., 4.]),
array([9., 6.]),
array([22., 13.]),
array([5., 1.]),
array([7., 2.]),
array([25., 26.]),
array([30., 6.]),
array([5., 7.]),
array([41., 13.]),
array([43., 12.]),
array([29., 14.]),
array([11., 6.]),
array([11., 7.]),
array([9., 5.]),
array([11., 4.]),
array([9., 6.]),
array([7., 0.]),
array([13., 5.]),
array([23., 15.]),
array([12., 6.]),
array([17., 2.]),
array([6., 2.]),
array([12., 4.]),
array([9., 5.]),
array([5., 1.]),
array([10., 5.]),
array([11., 8.]),
array([9., 8.]),
array([3., 4.]),
array([5., 3.]),
array([8., 4.]),
array([15., 4.]),
array([13., 2.]),
array([ 8., 17.]),
array([12., 11.]),
array([6., 4.]),
array([10., 3.]),
array([15., 2.]),
array([7., 2.]),
array([9., 4.]),
array([26., 7.]),
array([21., 8.]),
array([16., 9.]),
array([4., 2.]),
array([16., 4.]),
array([ 5., 12.]),
array([12., 3.]),
array([2., 2.]),
array([8., 3.]),
array([5., 0.]),
array([21., 5.]),
array([5., 1.]),
array([11., 5.]),
array([18., 15.]),
array([4., 7.]),
array([9., 5.]),
array([11., 0.]),
array([14., 4.]),
array([6., 2.]),
array([39., 21.]),
array([2., 2.]),
array([6., 0.]),
array([23., 9.]),
array([8., 6.]),
array([8., 5.]),
array([13., 6.]),
array([10., 2.]),
array([8., 5.]),
array([30., 7.]),
array([22., 8.]),
array([16., 4.]),
array([12., 3.]),
array([8., 4.]),
array([10., 3.]),
array([11., 9.]),
array([18., 7.]),
array([7., 1.]),
array([8., 3.]),
array([7., 2.]),
array([10., 3.]),
array([5., 0.]),
array([5., 5.]),
array([29., 20.]),
array([8., 1.]),
array([16., 17.]),
array([10., 6.]),
array([8., 3.]),
array([21., 17.]),
array([7., 0.]),
array([15., 1.]),
array([5., 6.]),
array([9., 6.]),
array([2., 0.]),
array([16., 15.]),
array([30., 30.]),
array([12., 0.]),
array([1., 0.]),
array([10., 3.]),
array([8., 1.]),
array([7., 2.]),
array([10., 1.]),
array([11., 7.]),
array([12., 3.]),
array([7., 6.]),
array([14., 3.]),
array([8., 5.]),
array([12., 5.]),
array([7., 1.]),
array([11., 7.]),
array([9., 6.]),
array([6., 3.]),
array([ 8., 10.]),
array([12., 6.]),
array([11., 1.]),
array([4., 0.]),
array([11., 12.]),
array([42., 21.]),
array([9., 3.]),
array([11., 2.]),
array([17., 9.]),
array([23., 19.]),
array([16., 0.]),
array([9., 2.]),
array([6., 4.]),
array([3., 4.]),
array([23., 21.]),
array([7., 4.]),
array([10., 2.]),
array([26., 0.]),
array([27., 8.]),
array([27., 25.]),
array([29., 4.]),
array([28., 6.]),
array([13., 0.]),
array([21., 23.]),
array([10., 4.]),
array([4., 6.]),
array([20., 7.]),
array([35., 25.]),
array([3., 1.]),
array([13., 3.]),
array([9., 9.]),
array([10., 2.]),
array([7., 7.]),
array([8., 4.]),
array([26., 19.]),
array([10., 1.]),
array([17., 2.]),
array([8., 2.]),
array([9., 5.]),
array([7., 4.]),
array([6., 7.]),
array([12., 9.]),
array([10., 8.]),
array([28., 6.]),
array([6., 1.]),
array([7., 0.]),
array([13., 7.]),
array([7., 2.]),
array([3., 1.]),
array([21., 4.]),
array([10., 11.]),
array([4., 1.]),
array([4., 1.]),
array([12., 13.]),
array([13., 16.]),
array([6., 6.]),
array([5., 3.]),
array([25., 16.]),
array([44., 34.]),
array([8., 6.]),
array([23., 19.]),
array([7., 6.]),
array([10., 3.]),
array([10., 4.]),
array([8., 1.]),
array([27., 20.]),
array([10., 14.]),
array([11., 1.]),
array([18., 4.]),
array([59., 17.]),
array([22., 16.]),
array([9., 3.]),
array([9., 3.]),
array([9., 3.]),
array([21., 17.]),
array([3., 3.]),
array([25., 16.]),
array([6., 1.]),
array([11., 7.]),
array([10., 5.]),
array([7., 4.]),
array([14., 36.]),
array([7., 1.]),
array([25., 21.]),
array([ 9., 14.]),
array([11., 0.]),
array([13., 5.]),
array([15., 0.]),
array([12., 3.]),
array([10., 3.]),
array([16., 10.]),
array([7., 6.]),
array([6., 4.]),
array([15., 2.]),
array([18., 4.]),
array([8., 2.]),
array([15., 0.]),
array([6., 2.]),
array([12., 6.]),
array([13., 1.]),
array([45., 12.]),
array([14., 9.]),
array([17., 7.]),
array([6., 5.]),
array([7., 3.]),
array([9., 2.]),
array([4., 3.]),
array([16., 1.]),
array([17., 11.]),
array([5., 0.]),
array([7., 8.]),
array([13., 2.]),
array([15., 9.]),
array([9., 2.]),
array([18., 36.]),
array([30., 2.]),
array([10., 2.]),
array([25., 8.]),
array([19., 13.]),
array([14., 2.]),
array([25., 5.]),
array([10., 4.]),
array([9., 8.]),
array([14., 6.]),
array([9., 3.]),
array([5., 2.]),
array([13., 3.]),
array([5., 1.]),
array([13., 4.]),
array([17., 2.]),
array([24., 26.]),
array([22., 5.]),
array([55., 21.]),
array([7., 4.]),
array([ 7., 13.]),
array([23., 21.]),
array([7., 0.]),
array([21., 15.]),
array([12., 3.]),
array([12., 2.]),
array([15., 10.]),
array([21., 5.]),
array([21., 10.]),
array([11., 1.]),
array([43., 22.]),
array([21., 15.]),
array([6., 3.]),
array([9., 0.]),
array([15., 2.]),
array([22., 3.]),
array([11., 4.]),
array([11., 2.]),
array([2., 2.]),
array([35., 7.]),
array([48., 20.]),
array([14., 22.]),
array([28., 24.]),
array([6., 1.]),
array([16., 3.]),
array([9., 2.]),
array([79., 17.]),
array([9., 1.]),
array([10., 15.]),
array([12., 1.]),
array([25., 19.]),
array([4., 0.]),
array([5., 0.]),
array([6., 3.]),
array([12., 5.]),
array([20., 7.]),
array([13., 6.]),
array([5., 0.]),
array([12., 0.]),
array([8., 3.]),
array([18., 1.]),
array([13., 16.]),
array([12., 4.]),
array([16., 2.]),
array([15., 1.]),
array([11., 0.]),
array([15., 8.]),
array([9., 9.]),
array([13., 13.]),
array([6., 3.]),
array([16., 4.]),
array([15., 4.]),
array([24., 19.]),
array([14., 10.])],
'neg': [array([6., 5.]),
array([8., 3.]),
array([12., 8.]),
array([ 5., 12.]),
array([12., 14.]),
array([18., 11.]),
array([11., 4.]),
array([4., 5.]),
array([12., 12.]),
array([5., 3.]),
array([5., 3.]),
array([23., 10.]),
array([5., 3.]),
array([17., 6.]),
array([11., 16.]),
array([9., 4.]),
array([9., 9.]),
array([29., 49.]),
array([7., 5.]),
array([13., 7.]),
array([9., 7.]),
array([41., 28.]),
array([12., 9.]),
array([4., 5.]),
array([10., 7.]),
array([6., 7.]),
array([1., 5.]),
array([14., 14.]),
array([10., 5.]),
array([9., 5.]),
array([7., 5.]),
array([22., 14.]),
array([5., 2.]),
array([ 6., 10.]),
array([6., 3.]),
array([12., 3.]),
array([ 6., 19.]),
array([ 6., 13.]),
array([3., 7.]),
array([5., 2.]),
array([0., 5.]),
array([14., 7.]),
array([11., 7.]),
array([12., 2.]),
array([6., 2.]),
array([3., 8.]),
array([3., 5.]),
array([ 5., 10.]),
array([16., 10.]),
array([5., 3.]),
array([8., 4.]),
array([15., 2.]),
array([24., 11.]),
array([18., 34.]),
array([8., 5.]),
array([3., 7.]),
array([4., 5.]),
array([20., 9.]),
array([ 7., 15.]),
array([60., 20.]),
array([ 6., 12.]),
array([ 4., 12.]),
array([ 9., 11.]),
array([9., 8.]),
array([20., 4.]),
array([14., 6.]),
array([22., 7.]),
array([35., 19.]),
array([12., 9.]),
array([8., 7.]),
array([23., 33.]),
array([0., 1.]),
array([4., 7.]),
array([1., 5.]),
array([11., 5.]),
array([5., 4.]),
array([7., 9.]),
array([22., 28.]),
array([7., 4.]),
array([10., 8.]),
array([ 6., 11.]),
array([18., 21.]),
array([4., 8.]),
array([ 7., 34.]),
array([10., 6.]),
array([3., 9.]),
array([22., 7.]),
array([8., 7.]),
array([5., 5.]),
array([12., 12.]),
array([ 5., 17.]),
array([22., 18.]),
array([13., 11.]),
array([3., 5.]),
array([10., 7.]),
array([7., 7.]),
array([ 4., 14.]),
array([32., 10.]),
array([5., 4.]),
array([3., 4.]),
array([14., 17.]),
array([9., 3.]),
array([12., 6.]),
array([9., 4.]),
array([6., 5.]),
array([4., 7.]),
array([5., 3.]),
array([12., 23.]),
array([26., 33.]),
array([10., 10.]),
array([21., 39.]),
array([15., 5.]),
array([2., 6.]),
array([12., 9.]),
array([9., 6.]),
array([12., 7.]),
array([16., 8.]),
array([50., 21.]),
array([13., 17.]),
array([26., 19.]),
array([4., 6.]),
array([6., 7.]),
array([13., 6.]),
array([ 8., 14.]),
array([13., 6.]),
array([16., 4.]),
array([ 5., 11.]),
array([8., 4.]),
array([4., 4.]),
array([8., 6.]),
array([ 8., 10.]),
array([11., 13.]),
array([7., 0.]),
array([35., 13.]),
array([6., 6.]),
array([3., 9.]),
array([7., 5.]),
array([5., 5.]),
array([11., 7.]),
array([8., 5.]),
array([20., 29.]),
array([7., 9.]),
array([7., 7.]),
array([31., 15.]),
array([9., 6.]),
array([9., 7.]),
array([ 5., 10.]),
array([ 8., 11.]),
array([5., 6.]),
array([5., 7.]),
array([22., 17.]),
array([4., 6.]),
array([ 8., 13.]),
array([7., 3.]),
array([29., 17.]),
array([3., 6.]),
array([10., 2.]),
array([7., 4.]),
array([7., 7.]),
array([52., 45.]),
array([9., 1.]),
array([11., 5.]),
array([23., 18.]),
array([5., 5.]),
array([4., 5.]),
array([13., 10.]),
array([ 9., 13.]),
array([7., 7.]),
array([2., 4.]),
array([11., 3.]),
array([14., 16.]),
array([3., 6.]),
array([6., 7.]),
array([4., 6.]),
array([10., 4.]),
array([7., 4.]),
array([15., 7.]),
array([6., 6.]),
array([4., 8.]),
array([4., 9.]),
array([37., 57.]),
array([18., 4.]),
array([4., 4.]),
array([6., 5.]),
array([7., 2.]),
array([8., 6.]),
array([5., 4.]),
array([15., 4.]),
array([3., 9.]),
array([38., 15.]),
array([13., 8.]),
array([ 9., 19.]),
array([7., 3.]),
array([15., 8.]),
array([52., 29.]),
array([23., 12.]),
array([10., 10.]),
array([8., 4.]),
array([3., 4.]),
array([31., 30.]),
array([17., 16.]),
array([4., 1.]),
array([12., 9.]),
array([6., 5.]),
array([16., 20.]),
array([4., 8.]),
array([2., 5.]),
array([ 5., 18.]),
array([17., 13.]),
array([2., 7.]),
array([12., 11.]),
array([ 7., 10.]),
array([ 8., 19.]),
array([ 3., 13.]),
array([13., 18.]),
array([5., 4.]),
array([ 7., 11.]),
array([4., 3.]),
array([ 3., 10.]),
array([10., 10.]),
array([2., 2.]),
array([1., 0.]),
array([8., 9.]),
array([ 5., 11.]),
array([20., 11.]),
array([9., 3.]),
array([2., 2.]),
array([18., 6.]),
array([ 7., 11.]),
array([11., 14.]),
array([6., 6.]),
array([31., 50.]),
array([25., 11.]),
array([0., 7.]),
array([13., 11.]),
array([14., 1.]),
array([11., 10.]),
array([6., 8.]),
array([16., 21.]),
array([28., 15.]),
array([1., 2.]),
array([4., 6.]),
array([13., 19.]),
array([7., 6.]),
array([4., 1.]),
array([23., 13.]),
array([6., 2.]),
array([15., 17.]),
array([6., 9.]),
array([23., 28.]),
array([7., 6.]),
array([22., 13.]),
array([15., 14.]),
array([15., 10.]),
array([ 8., 11.]),
array([19., 20.]),
array([23., 8.]),
array([9., 5.]),
array([7., 9.]),
array([12., 7.]),
array([16., 14.]),
array([7., 4.]),
array([11., 11.]),
array([ 8., 14.]),
array([5., 8.]),
array([4., 4.]),
array([2., 3.]),
array([12., 9.]),
array([5., 3.]),
array([10., 12.]),
array([9., 1.]),
array([12., 3.]),
array([7., 9.]),
array([14., 12.]),
array([6., 9.]),
array([7., 2.]),
array([9., 4.]),
array([13., 14.]),
array([6., 3.]),
array([10., 3.]),
array([2., 2.]),
array([4., 3.]),
array([18., 25.]),
array([3., 5.]),
array([10., 5.]),
array([11., 5.]),
array([ 8., 10.]),
array([5., 6.]),
array([10., 4.]),
array([4., 4.]),
array([14., 13.]),
array([5., 7.]),
array([ 8., 24.]),
array([14., 10.]),
array([19., 27.]),
array([22., 26.]),
array([7., 0.]),
array([ 6., 10.]),
array([6., 3.]),
array([7., 1.]),
array([10., 3.])]})
3. Écrire un classifieur logistique qui prend en entrée les vecteurs de features précédents et utilise les poids respectifs $0.6$ et $-0.4$ et un biais de $0$. Appliquez ce classifieur sur le mini-corpus IMDB et calculez son exactitude.
On commence par définir le classifieur : on va renvoyer True pour la classe positive et False
pour la classe négative.
def hardcoded_classifier(x):
return (
np.inner(
np.array([0.6, -0.4]),
x,
)
> 0.0
)
hardcoded_classifier(doc_features)
True
Maintenant on le teste
correct_pos = sum(1 for doc in imdb_features["pos"] if hardcoded_classifier(doc))
print(f"Recall for 'pos': {correct_pos}/{len(imdb_features['pos'])}={correct_pos/len(imdb_features['pos']):.02%}")
correct_neg = sum(1 for doc in imdb_features["neg"] if not hardcoded_classifier(doc))
print(f"Recall for 'neg': {correct_neg}/{len(imdb_features['neg'])}={correct_neg/len(imdb_features['neg']):.02%}")
print(f"Accuracy: {correct_pos+correct_neg}/{len(imdb_features['pos'])+len(imdb_features['neg'])}={(correct_pos+correct_neg)/(len(imdb_features['pos'])+len(imdb_features['neg'])):.02%}")
Recall for 'pos': 291/301=96.68% Recall for 'neg': 68/301=22.59% Accuracy: 359/602=59.63%
Elle permet de normaliser $z$ : $z$ peut être n'importe quel nombre entre $-∞$ et $+∞$, mais on aura toujours $0 < σ(z) < 1$, ce qui permet de l'interpréter facilement comme une vraisemblance. Autrement dit, $σ(z)$ sera proche de $1$ s'il paraît vraisemblable que $x$ appartienne à la classe $1$ et proche de $0$ sinon.
Tracer avec matplotlib la courbe représentative de la fonction logistique.
def logistic(z):
return 1/(1+np.exp(-z))
%matplotlib inline
import matplotlib.pyplot as plt
x = np.linspace(-10, 10, 5000)
y = logistic(x)
plt.plot(x, y)
plt.xlabel("$x$")
plt.ylabel("$σ(x)$")
plt.title("Courbe représentative de la fonction logistique sur $[-10, 10]$")
plt.show()
Formellement : on suppose qu'il existe une fonction $f$ qui prédit parfaitement les classes, donc telle que pour tout couple exemple/étiquette $(x, y)$ avec $y$ valant $0$ ou $1$, $f(x) = y$. On approcher cette fonction par une fonction $g$ de la forme
$$g(x) = σ(w⋅x+b)$$Si on choisit les poids $w$ et le biais $b$ tels que $g$ soit la plus proche possible de $f$ sur notre ensemble d'apprentissage, on dit que $g$ est la régression logistique de $f$ sur cet ensemble.
Un classifieur logistique, c'est simplement un classifieur qui pour un exemple $x$ renvoie $0$ si $g(x) < 0.5$ et $1$ sinon. Il a exactement les mêmes capacités de discrimination qu'un classifieur linéaire (il ne sait pas prendre de décisions plus complexes), mais on peut interpréter la confiance qu'il a dans sa décision.
Par exemple voici la confiance que notre classifieur codé en dur a en ses décisions
def classifier_confidence(x):
return logistic(
np.inner(
np.array([0.6, -0.4]),
x,
)
)
classifier_confidence(doc_features)
0.9986414800495711
Autrement dit, d'après notre classifieur, il y a un peu plus de $99.86\%$ de chances que le document soit de la classe $1$ (review positive dans ce cas), ou encore, la vraisemblance de la classe $1$ est vraisemblable à plus de $99.68\%$.
Quelle est la vraisemblance de la classe $0$ (review négative) ? Et bien le reste
1.0 - classifier_confidence(doc_features)
0.0013585199504289047
Soit un peu plus de $0.1\%$.
Comme l'exemple en question appartient bien à cette classe, ça signifie que notre classifieur et plutôt bon sur cet exemple. L'est-il sur le reste du corpus ?
pos_confidence = sum(classifier_confidence(doc) for doc in imdb_features["pos"])
print(f"Average confidence for 'pos': {pos_confidence/len(imdb_features['pos']):.02%}")
neg_confidence = sum(1-classifier_confidence(doc) for doc in imdb_features["neg"])
print(f"Average confidence for 'neg': {neg_confidence/len(imdb_features['neg']):.02%}")
print(f"Average confidence for the correct class: {(pos_confidence+neg_confidence)/(len(imdb_features['pos']) + len(imdb_features['neg'])):.02%}")
Average confidence for 'pos': 93.07% Average confidence for 'neg': 25.86% Average confidence for the correct class: 59.47%
Autrement dit, pour un exemple pris au hasard dans le corpus, la vraisemblance de sa classe telle que jugée par le classifieur sera de $59.47\%$. Un classifieur parfait obtiendrait $100\%$, un classifieur qui prendrait systématiquement la mauvaise décision $0\%$ et un classifieur aléatoire uniforme $50\%$ (puisque notre corpus a autant d'exemples de chaque classe).
Moralité nos poids ne sont pas très bien choisis, et notre préoccupation dans la suite va être de chercher comment choisir des poids pour que la confiance moyenne de la classe correcte soit aussi haute que possible.
On a dit que notre objectif était
Chercher les poids $w$ et le biais $b$ tels que $g$ soit la plus proche possible de $f$ sur notre ensemble d'apprentissage
On formalise « être le plus proche possible » de la section précédente comme minimiser une certaine fonction de coût (loss) $L$ qui mesure l'erreur faite par le classifieur sur un exemple.
$$L(g(x), y) = \text{l'écart entre la classe prédite par $g$ pour $x$ et la classe correcte $y$}$$Étant donné un ensemble de test $(x₁, y₁), …, (x_n, y_n)$, on estime l'erreur faite par le classifieur logistique $g$ pour chaque exemple $(x_i, y_i)$ comme le coût local $L(g(xᵢ), yᵢ)$ et son erreur sur tout l'ensemble de test par le coût global $\mathcal{L}$ :
$$\mathcal{L} = \sum_i L(g(xᵢ), yᵢ)$$Plus $\mathcal{L}$ sera bas, meilleur sera notre classifieur.
Dans le cas de la régression logistique, on va s'inspirer de ce qu'on a vu dans la section précédente et utiliser la log-vraisemblance négative (negative log-likelihood) :
On définit la vraisemblance $V$ comme précédemment par $$ V(a, y) = \begin{cases} a & \text{si $y = 1$}\\ 1-a & \text{sinon} \end{cases} $$
Intuitivement, il s'agit de la vraisemblance affectée par le modèle à la classe correcte $y$. Il ne s'agit donc pas d'un coût, mais d'un gain (si sa valeur est haute, c'est que le modèle est bon)
La log-vraisemblance négative $L$ est alors définie par
$$L(a, y) = -\log(V(a, y))$$Le $\log$ est là pour plusieurs raisons, calculatoires et théoriques1 et le $-$ à s'assurer qu'on a bien un coût (plus la valeur est basse, meilleur le modèle est).
1. Entre autres, comme pour *Naïve Bayes*, parce qu'une somme de $\log$-vraisemblance peut être vue comme le $\log$ de la probabilité d'une conjonction d'événements indépendants. Mais surtout parce qu'il rend la fonction de coût **convexe** par rapport à $w$.
Une interprétation possible : $L(a, y)$, c'est la surprise de $y$ au sens de la théorie de l'information. Autrement dit : si j'estime qu'il y a une probabilité $a$ d'observer la classe $y$, $L(a, y)$ mesure à quel point il serait surprenant d'observer effectivement $y$.
On peut vérifier qu'il s'agit bien d'un coût :
Si le classifieur prend une décision correcte avec une confiance parfaite le coût est nul :
$$ \begin{cases}
L(1.0, 1) = -\log(1.0) = 0\\
L(0.0, 0) = -\log(1.0-0.0) = -\log(1.0) = 0
\end{cases} $$
Si le classifieur prend une décision erronée avec une confiance parfaite le coût est infini :
$$ \begin{cases}
L(0.0, 1) = -\log(0.0) = +\infty\\
L(1.0, 0) = -\log(1.0-1.0) = \log(0.0) = +\infty
\end{cases} $$
On peut aussi vérifier facilement que $L(a, 1)$ est décroissant par rapport à $a$ et que $L(1-a, 0)$ est croissant par rapport à $a$. Autrement dit, plus le classifieur juge que la classe correcte est vraisemblable plus le coût $L$ est bas.
Enfin, on peut l'écrire $L$ en une ligne : pour un exemple $x$, le coût de l'exemple $(x, y)$ est
$$L(g(x), y) = -\log\left[g(x)×y + (1-g(x))×(1-y)\right]$$C'est un trick, l'astuce c'est que comme $y$ vaut soit $0$ soit $1$, soit $y=0$, soit $1-y=0$ et donc la somme dans le $\log$ se simplifie dans tous les cas. Rien de transcendant là-dedans.
La formule diffère un peu de celle de Speech and Language Processing, mais les résultats sont les mêmes et celle-ci est mieux pour notre problème !
En fait la leur est la formule générale de l'entropie croisée pour des distributions de proba à support dans $\{0, 1\}$, ce qui est une autre intuition pour cette fonction de coût, mais ici elle nous complique la vie.
Une dernière façon de l'écrire en une ligne :
$$L(g(x), y) = -\log\left[g(x)\mathbb{1}_{y=1} + (1-g(x))\mathbb{1}_{y=0}\right]$$Écrire une fonction qui prend en entrée
Et renvoie la log-vraisemblance négative du classifieur logistique de poids $(w, b)$ pour l'exemple $(x, y)$.
Servez-vous en pour calculer le coût du classifieur de l'exercise précédent sur le mini-corpus IMDB.
def logistic_negative_log_likelihood(x, w, b, y):
g_x = logistic(np.inner(w, x) + b)
if y == 1:
correct_likelihood = g_x
else:
correct_likelihood = 1-g_x
loss = -np.log(correct_likelihood)
return loss
def loss_on_imdb(w, b, featurized_corpus):
loss_on_pos = np.zeros(1)
for doc_features in featurized_corpus["pos"]:
loss_on_pos += logistic_negative_log_likelihood(
doc_features,
w,
b,
1,
)
loss_on_neg = np.zeros(1)
for doc_features in featurized_corpus["neg"]:
loss_on_neg += logistic_negative_log_likelihood(
doc_features,
w,
b,
0,
)
return loss_on_pos + loss_on_neg
Avec des compréhensions
def loss_on_imdb(w, b, featurized_corpus):
loss_on_pos = sum(
logistic_negative_log_likelihood(
doc_features,
w,
b,
1,
)
for doc_features in featurized_corpus["pos"]
)
loss_on_neg = sum(
logistic_negative_log_likelihood(
doc_features,
w,
b,
0,
)
for doc_features in featurized_corpus["neg"]
)
return loss_on_pos + loss_on_neg
En version numériquement stable
import math
def loss_on_imdb(w, b, featurized_corpus):
loss_on_pos = math.fsum(
logistic_negative_log_likelihood(
doc_features,
w,
b,
1,
).astype(float)
for doc_features in featurized_corpus["pos"]
)
loss_on_neg = math.fsum(
logistic_negative_log_likelihood(
doc_features,
w,
b,
0,
).astype(float)
for doc_features in featurized_corpus["neg"]
)
return loss_on_pos + loss_on_neg
loss_on_imdb(np.array([0.6, -0.4]), 0, imdb_features)
994.3233745292855
L'algorithme de descente de gradient est la clé de voute de l'essentiel des travaux en apprentissage artificiel moderne. Il s'agit d'un algorithme itératif qui étant donné un modèle paramétrisé et une fonction de coût (avec des hypothèses de régularité assez faibles) permet de trouver des valeurs des paramètres pour lesquelles la fonction de coût est minimal.
On ne va pas rentrer dans les détails de l'algorithme de descente de gradient stochastique, mais juste essayer de se donner quelques idées.
L'intuition à avoir est la suivante : si vous êtes dans une vallée et que vous voulez trouver rapidement le point le plus bas, une façon de faire est de chercher la direction vers laquelle la pente descend le plus vite, de faire quelques pas dans cette direction puis de recommencer. On parle aussi pour cette raison d'algorithme de la plus forte pente.
Clairement une condition pour que ça marche peu importe le point de départ, c'est que la vallée n'ait qu'un seul point localement le plus bas. Par exemple ça marche avec une vallée comme celle-ci
%matplotlib inline
import tol_colors as tc
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(figsize=(20, 20), dpi=200)
ax = plt.axes(projection='3d')
r = np.linspace(0, 8, 100)
p = np.linspace(0, 2*np.pi, 100)
R, P = np.meshgrid(r, p)
Z = R**2 - 1
X, Y = R*np.cos(P), R*np.sin(P)
ax.plot_surface(X, Y, Z, cmap=tc.tol_cmap("sunset"), edgecolor="none", rstride=1, cstride=1)
ax.plot_wireframe(X, Y, Z, color='black')
plt.show()
Mais pas pour celle-là
%matplotlib inline
import tol_colors as tc
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(figsize=(20, 20), dpi=200)
ax = plt.axes(projection='3d')
r = np.linspace(0, 8, 100)
p = np.linspace(0, 2*np.pi, 100)
R, P = np.meshgrid(r, p)
Z = -np.cos(R)/(1+0.5*R**2)
X, Y = R*np.cos(P), R*np.sin(P)
ax.plot_surface(X, Y, Z, cmap=tc.tol_cmap("sunset"), edgecolor="none", rstride=1, cstride=1)
#ax.plot_wireframe(X, Y, Z, color='black')
plt.show()
OK, mais comment on trouve la plus forte pente en pratique ? En une dimension il suffit de suivre l'opposé du nombre dérivé : https://uclaacm.github.io/gradient-descent-visualiser/#playground
En plus de dimensions, c'est plus compliqué, mais on peut s'en sortir en suivant le gradient qui est une généralisation du nombre dérivé : https://jackmckew.dev/3d-gradient-descent-in-python.html
Ce qui fait marcher la machine c'est que le gradient indique la direction dans laquelle la fonction croît le plus vite. Et que l'opposé du gradient indique la direction dans laquelle la fonction décroît le plus vite.
(localement)
Concrètement si on veut trouver $\theta$ tel que $f(\theta)$ soit minimale pour une certaine
fonction $f$ dont le gradient est donné par grad_f ça donne l'algo suivant
def descent(grad_f, theta_0, learning_rate, n_steps):
theta = theta_0
for _ in range(n_steps):
# On trouve la direction de plus grande pente
steepest_direction = -grad_f(theta)
# On fait quelques pas dans cette direction
theta += learning_rate*steepest_direction
return theta
Les hyperparamètres sont
theta_0 est notre point de départ, notre première estimation d'où se trouvera le minimum, que
l'algorithme va raffiner. Évidemment si on a déjà une idée de vers où on pourrait le trouver, ça
ira plus vite. Si on a aucune idée, on peut le prendre aléatoire.learning_rate ou « taux d'apprentissage » : de combien on se déplace à chaque étape. Si on le
prend grand on arrive vite vers la région du minimum, on mettra longtemps pour en trouver une
approximation précise. Si on le prend petit, ça sera l'inverse.n_steps est le nombre d'étapes d'optimisations. Dans un problème d'apprentissage, c'est aussi
le nombre de fois où on aura parcouru l'ensemble d'apprentissage et on parle souvent d'epochIci on se donne un nombre fixe d'epochs, une autre possibilité serait de s'arrêter quand on ne bouge plus trop, par exemple avec une condition comme
if np.max(grad_f(theta)) < 0.00001:
break
dans la boucle et éventuellement avec une boucle infinie while True.
Point notation :
Rappelez-vous, on a dit que notre fonction de coût, c'était
$$\mathcal{L} = \sum_i L(g(xᵢ), yᵢ)$$et on cherche la valeur du paramètre $θ = (w_1, …, w_n, b)$ tel que $\mathcal{L}$ soit le plus petit possible.
On peut utilise la propriété d'additivité du gradient : pour deux fonctions $f$ et $g$, on a
$$\operatorname{grad}(f+g) = \operatorname{grad}f + \operatorname{grad}g$$Donc ici
$$\operatorname{grad}\mathcal{L} = \sum_i \operatorname{grad}L(g(xᵢ), yᵢ)$$Si on dispose d'une fonction grad_L qui, étant donnés $g(x_i)$ et $y_i$, renvoie
$\operatorname{grad}L(g(x_i), y_i)$, l'algorithme de descente du gradient devient alors
def descent(train_set, theta_0, learning_rate, n_steps):
theta = theta_0
for _ in range(n_steps):
w = theta[:-1]
b = theta[-1]
partial_grads = []
for (x, y) in train_set:
# On calcule g(x)
g_x = logistic(np.inner(w,x)+b)
# On calcule le gradient de L(g(x), y))
partial_grads.append(grad_L(g_x, y))
# On trouve la direction de plus grande pente
steepest_direction = -np.sum(partial_grads)
# On fait quelques pas dans cette direction
theta += learning_rate*steepest_direction
return theta
Pour chaque étape, on doit calculer tous les $g(x_i)$ et $\operatorname{grad}L(g(x_i), y_i)$. C'est très couteux, il doit y avoir moyen de faire mieux.
Si les $L(g(xᵢ), yᵢ)$ étaient indépendants, ce serait plus simple : on pourrait les optimiser séparément.
Ce n'est évidemment pas le cas : si on change $g$ pour que $g(x_0)$ soit plus proche de $y_0$, ça changera aussi la valeur de $g(x_1)$.
Mais on va faire comme si
C'est une approximation sauvage, mais après tout on commence à avoir l'habitude. On va donc suivre l'algo suivant
def descent(train_set, theta_0, learning_rate, n_steps):
theta = theta_0
for _ in range(n_steps):
for (x, y) in train_set:
w = theta[:-1]
b = theta[-1]
# On calcule g(x)
g_x = logistic(np.inner(w,x)+b)
# On trouve la direction de plus grande pente
steepest_direction = -grad_L(g_x, y)
# On fait quelques pas dans cette direction
theta += learning_rate*steepest_direction
return theta
Faites bien attention à la différence : au lieu d'attendre d'avoir calculé tous les $\operatorname{grad}L(g(x_i), y_i)$ avant de modifier $θ$, on va le modifier à chaque fois.
Notre espoir ici c'est que cette situation n'arrivera pas, et qu'on bon paramètre pour un certain
couple $(x, y)$ c'est un bon paramètres pour $tous$ les couples (exemple, classe).
Ce nouvel algorithme s'appelle l'algorithme de descente de gradient stochastique, et il est crucial pour nous, parce qu'on ne pourra en pratique quasiment jamais faire de descente de gradient globale.
Il ne nous reste plus qu'à savoir comment on calcule grad_L. On ne fera pas la preuve, mais on a
et
Autrement dit on mettra à jour $w$ en calculant
$$w ← w -η×\operatorname{d}_wL(g(x), y) = w - η×(g(x)-y)x$$$\operatorname{d}_wL(g(x), y) = \left(\frac{∂L(g(x), y)}{∂w_1}, …, \frac{∂L(g(x), y)}{∂w_n}\right)$ est la *différentielle partielle* de $L(g(x), y)$ par rapport à $w$.
Et $b$ en calculant
$$b ← b -η×\frac{∂L(g(x), y)}{∂b} = b - η×(g(x)-y)$$1. Reprendre la fonction qui calcule la fonction de coût, et la transformer pour qu'elle renvoie le gradient par rapport à $w$ et la dérivée partielle par rapport à $b$ en $(x, y)$.
def grad_L(x, w, b, y):
grad = np.zeros(w.size+b.size) # À vous !
return grad
grad_L(np.array([5, 10]), np.array([0.6, -0.4]), np.array([0.0]), 1)
array([0., 0., 0.])
def grad_L(x, w, b, y):
g_x = logistic(np.inner(w, x) + b)
grad_w = (g_x - y)*x
grad_b = g_x - y
return np.append(grad_w, grad_b)
2. S'en servir pour apprendre les poids à donner aux features précédentes à l'aide du mini-corpus IMDB en utilisant l'algorithme de descente de gradient stochastique.
def descent(featurized_corpus, theta_0, learning_rate, n_steps):
theta = theta_0
for _ in range(n_steps):
pass # À vous !
return
descent(imdb_features, np.array([0.6, -0.4, 0.0]), 0.001, 100)
import random
def descent(featurized_corpus, theta_0, learning_rate, n_steps):
train_set = [
*((doc, 1) for doc in featurized_corpus["pos"]),
*((doc, 0) for doc in featurized_corpus["neg"])
]
theta = theta_0
w = theta[:-1]
b = theta[-1]
print(f"Initial loss: {loss_on_imdb(w, b, featurized_corpus)}")
for i in range(n_steps):
# On mélange le corpus pour s'assurer de ne pas avoir d'abord tous
# les positifs puis tous les négatifs
random.shuffle(train_set)
for j, (x, y) in enumerate(train_set):
grad = grad_L(x, w, b, y)
steepest_direction = -grad
# Purement pour l'affichage
loss = logistic_negative_log_likelihood(x, w, b, y)
#print(f"step {i*len(train_set)+j} doc={x}\tw={w}\tb={b}\tloss={loss}\tgrad={grad}")
theta += learning_rate*steepest_direction
w = theta[:-1]
b = theta[-1]
print(f"Epoch {i} loss: {loss_on_imdb(w, b, featurized_corpus)}\tw={w}\tb={b}")
return (theta[:-1], theta[-1])
descent(imdb_features, np.array([0.6, -0.4, 0.0]), 0.001, 100)
Initial loss: 994.3233745292855 Epoch 0 loss: 423.34557940280564 w=[ 0.16069573 -0.33052659] b=-0.029470685498494423 Epoch 1 loss: 370.51731848045847 w=[ 0.12847826 -0.17497934] b=-0.03294337207341621 Epoch 2 loss: 373.48859925799775 w=[ 0.08672489 -0.15384082] b=-0.04108179365195779 Epoch 3 loss: 369.5590373381049 w=[ 0.10371455 -0.13557622] b=-0.05052009139841104 Epoch 4 loss: 369.1229885939176 w=[ 0.11953821 -0.16690838] b=-0.05802473338661318 Epoch 5 loss: 374.41092122219527 w=[ 0.13429885 -0.16001431] b=-0.06124027938694548 Epoch 6 loss: 380.8419006670716 w=[ 0.12509362 -0.12953635] b=-0.06616754349849746 Epoch 7 loss: 379.0746083236867 w=[ 0.12339898 -0.13013079] b=-0.07209717771941564 Epoch 8 loss: 411.661174779387 w=[ 0.06461735 -0.17926379] b=-0.07994982334627813 Epoch 9 loss: 385.2484114589694 w=[ 0.13767369 -0.13796473] b=-0.08015962664590404 Epoch 10 loss: 425.01243665219755 w=[ 0.06589583 -0.19556281] b=-0.09032444750246335 Epoch 11 loss: 368.76274636659394 w=[ 0.10999341 -0.16063909] b=-0.09379474793252875 Epoch 12 loss: 377.6510906658865 w=[ 0.13574805 -0.14906055] b=-0.09533954058862622 Epoch 13 loss: 369.7472716069027 w=[ 0.10641584 -0.16337517] b=-0.1023431575532521 Epoch 14 loss: 370.00614374568886 w=[ 0.10377924 -0.16089903] b=-0.10715682059110923 Epoch 15 loss: 374.6287451940752 w=[ 0.14274466 -0.1683363 ] b=-0.1116451748509835 Epoch 16 loss: 407.48785332109463 w=[ 0.07641485 -0.19055126] b=-0.11722633203278522 Epoch 17 loss: 369.31732727605845 w=[ 0.12033272 -0.15376135] b=-0.11701844270736535 Epoch 18 loss: 369.074808478806 w=[ 0.10004395 -0.14806688] b=-0.11969401910335155 Epoch 19 loss: 370.6854511915953 w=[ 0.11098757 -0.17250804] b=-0.12233902098835513 Epoch 20 loss: 372.312670158448 w=[ 0.11053561 -0.17809958] b=-0.1258556392404819 Epoch 21 loss: 369.9472643409114 w=[ 0.12940508 -0.18348566] b=-0.12790882994098768 Epoch 22 loss: 380.9999841035776 w=[ 0.09534288 -0.17855414] b=-0.13224552116116803 Epoch 23 loss: 385.5443112059346 w=[ 0.08561592 -0.17201953] b=-0.13281337819505357 Epoch 24 loss: 370.40250399346473 w=[ 0.10463146 -0.16203607] b=-0.1339117903010596 Epoch 25 loss: 392.01751351484506 w=[ 0.14877044 -0.13808579] b=-0.13276167332666228 Epoch 26 loss: 381.852325968532 w=[ 0.09702154 -0.18218177] b=-0.13877558819668656 Epoch 27 loss: 380.27912395648195 w=[ 0.09320323 -0.17288961] b=-0.1424910179769862 Epoch 28 loss: 374.179492149614 w=[ 0.1256166 -0.13816584] b=-0.14282628167928202 Epoch 29 loss: 372.95173740950054 w=[ 0.08623027 -0.14147259] b=-0.1453140062550528 Epoch 30 loss: 375.73175101207494 w=[ 0.09931103 -0.17127573] b=-0.14520674541408088 Epoch 31 loss: 378.9331351045397 w=[ 0.10885648 -0.1921584 ] b=-0.14614599056594954 Epoch 32 loss: 368.89915896565407 w=[ 0.1240256 -0.16845276] b=-0.14569566990441077 Epoch 33 loss: 381.03857630927644 w=[ 0.14133423 -0.14475314] b=-0.14659673476928053 Epoch 34 loss: 372.7389342918173 w=[ 0.10837628 -0.17487856] b=-0.15003238020395068 Epoch 35 loss: 386.38968288722367 w=[ 0.15127954 -0.14951257] b=-0.14794975021072893 Epoch 36 loss: 387.71291730633084 w=[ 0.07650819 -0.15872014] b=-0.1528741375550952 Epoch 37 loss: 376.7520026714101 w=[ 0.1291493 -0.13559501] b=-0.1532967734148336 Epoch 38 loss: 440.1273842920937 w=[ 0.16038009 -0.10205857] b=-0.15082083085117054 Epoch 39 loss: 374.44302929190735 w=[ 0.12014808 -0.12878566] b=-0.15435432915709407 Epoch 40 loss: 372.81813488056594 w=[ 0.10041785 -0.16352919] b=-0.1552484270378892 Epoch 41 loss: 374.68663867881446 w=[ 0.13827054 -0.15525326] b=-0.15369537004179296 Epoch 42 loss: 368.8016567862685 w=[ 0.11626253 -0.1468218 ] b=-0.15725194510045 Epoch 43 loss: 368.57793329754315 w=[ 0.11966032 -0.16283543] b=-0.1581584967494189 Epoch 44 loss: 372.0054384551456 w=[ 0.1017391 -0.16230094] b=-0.15936867134197294 Epoch 45 loss: 375.3354440182169 w=[ 0.13615512 -0.14924744] b=-0.1591627778478754 Epoch 46 loss: 381.99409646725235 w=[ 0.15037167 -0.15580628] b=-0.15724341646060974 Epoch 47 loss: 369.5063105329027 w=[ 0.12460091 -0.15442255] b=-0.162510254019094 Epoch 48 loss: 376.81740251181895 w=[ 0.09772613 -0.16980888] b=-0.16579913508229788 Epoch 49 loss: 401.6295626686682 w=[ 0.08156269 -0.18647361] b=-0.16875163687442366 Epoch 50 loss: 368.87176007227436 w=[ 0.11124623 -0.15839931] b=-0.1737056547862801 Epoch 51 loss: 368.4640911598861 w=[ 0.11171478 -0.14285434] b=-0.17028344011028182 Epoch 52 loss: 370.56442914633715 w=[ 0.1252509 -0.14819759] b=-0.16958631427715223 Epoch 53 loss: 368.8201070852426 w=[ 0.11052292 -0.15740405] b=-0.16907333990422016 Epoch 54 loss: 371.8721188787397 w=[ 0.11035819 -0.17299475] b=-0.168131529624577 Epoch 55 loss: 381.20889510447273 w=[ 0.09683438 -0.17802396] b=-0.16748156781599863 Epoch 56 loss: 373.1995010647545 w=[ 0.12994104 -0.14526279] b=-0.16738590825800007 Epoch 57 loss: 378.5499883497142 w=[ 0.14759424 -0.15805488] b=-0.16995143086808107 Epoch 58 loss: 368.8999440003033 w=[ 0.12584528 -0.16429651] b=-0.17385924417872778 Epoch 59 loss: 369.31943694775896 w=[ 0.13043085 -0.17262364] b=-0.17356784168949055 Epoch 60 loss: 372.32819057101074 w=[ 0.14390397 -0.174199 ] b=-0.1704964707419488 Epoch 61 loss: 368.79997093009865 w=[ 0.10639749 -0.15114367] b=-0.17321459684495927 Epoch 62 loss: 389.2231882997228 w=[ 0.08170625 -0.16776509] b=-0.17612179072827241 Epoch 63 loss: 392.5575722087589 w=[ 0.14781991 -0.13215401] b=-0.1730138702121864 Epoch 64 loss: 375.1645694732449 w=[ 0.13777179 -0.15060686] b=-0.17624028484210572 Epoch 65 loss: 369.1716793457704 w=[ 0.1257512 -0.17318585] b=-0.18030805587322526 Epoch 66 loss: 396.2622388068874 w=[ 0.16368925 -0.149551 ] b=-0.17786454719853667 Epoch 67 loss: 422.22971656556103 w=[ 0.17382639 -0.13279876] b=-0.18307605358346124 Epoch 68 loss: 369.0709364303904 w=[ 0.12232348 -0.15119355] b=-0.1882723351817378 Epoch 69 loss: 384.02403917005364 w=[ 0.08830346 -0.16800846] b=-0.1906191620342681 Epoch 70 loss: 383.151711985403 w=[ 0.08362951 -0.15873855] b=-0.18972275045655682 Epoch 71 loss: 369.6651780121425 w=[ 0.1247929 -0.15096726] b=-0.18637691225798733 Epoch 72 loss: 369.14050923696516 w=[ 0.11036506 -0.15818547] b=-0.18791763373736808 Epoch 73 loss: 370.745949123762 w=[ 0.13327575 -0.1599997 ] b=-0.18232917833337436 Epoch 74 loss: 396.17730744363564 w=[ 0.0823832 -0.17907466] b=-0.18108194633059754 Epoch 75 loss: 431.1858771266324 w=[ 0.05766483 -0.17802505] b=-0.18389265509916156 Epoch 76 loss: 376.34520974831116 w=[ 0.12201822 -0.12445539] b=-0.17937635582015124 Epoch 77 loss: 377.88011760907216 w=[ 0.14405084 -0.15314127] b=-0.17768815636837143 Epoch 78 loss: 373.38449163193326 w=[ 0.15155989 -0.18460289] b=-0.18090554466384384 Epoch 79 loss: 388.1428191148889 w=[ 0.1641123 -0.16357734] b=-0.17876575513523746 Epoch 80 loss: 376.0431936700937 w=[ 0.13690619 -0.14641449] b=-0.17968644346493415 Epoch 81 loss: 379.1564010637874 w=[ 0.15355491 -0.1656446 ] b=-0.17962336669140147 Epoch 82 loss: 383.39357846219065 w=[ 0.13023454 -0.12141832] b=-0.1817777613728679 Epoch 83 loss: 374.88925000208053 w=[ 0.1268089 -0.13418761] b=-0.18338813820490324 Epoch 84 loss: 369.7274610342414 w=[ 0.11193763 -0.16414204] b=-0.18512066386806594 Epoch 85 loss: 374.4292770748504 w=[ 0.14607095 -0.16661928] b=-0.18475673634968529 Epoch 86 loss: 370.0905150052491 w=[ 0.12749266 -0.1533136 ] b=-0.18237081689498771 Epoch 87 loss: 409.58078031158215 w=[ 0.15186262 -0.11646442] b=-0.17788679045453648 Epoch 88 loss: 397.7196609834878 w=[ 0.06930118 -0.15906387] b=-0.18093406980440888 Epoch 89 loss: 397.50210333005674 w=[ 0.14534621 -0.12189357] b=-0.17537709985528052 Epoch 90 loss: 372.6622480273861 w=[ 0.13093392 -0.14775022] b=-0.17681950192645274 Epoch 91 loss: 373.288296677764 w=[ 0.09579768 -0.15568117] b=-0.17948149088663015 Epoch 92 loss: 368.5910726840741 w=[ 0.12202524 -0.15985193] b=-0.18051579121539052 Epoch 93 loss: 379.1566526991361 w=[ 0.10777247 -0.18815271] b=-0.18168129867127455 Epoch 94 loss: 384.37961427443054 w=[ 0.10307479 -0.1919633 ] b=-0.1811568138270608 Epoch 95 loss: 370.32115975318925 w=[ 0.13683863 -0.17197068] b=-0.1807482205362884 Epoch 96 loss: 375.00155624644253 w=[ 0.14286694 -0.15907938] b=-0.1817080982846696 Epoch 97 loss: 368.8344829046819 w=[ 0.11826037 -0.16460144] b=-0.18689591850732504 Epoch 98 loss: 372.16562204573574 w=[ 0.12801395 -0.14428288] b=-0.1842046971799866 Epoch 99 loss: 385.4247853293311 w=[ 0.1400181 -0.13128494] b=-0.18280829695521217
(array([ 0.1400181 , -0.13128494]), -0.18280829695521217)
Un dernier point : on a vu dans tout ceci comment utiliser la régression logistique pour un problème de classification à deux classes. Comment on l'étend à $n$ classes ?
Réflechissons déjà à quoi ressemblerait la sortie d'un tel classifieur :
Pour un problème à deux classes, le classifieur $g$ nous donne pour chaque exemple $x$ une estimation $g(x)$ de la vraisemblance de la classe $1$, et on a vu que la vraisemblance de la classe $0$ était nécessairement $1-g(x)$ pour que la somme des vraisemblances fasse 1.
On peut le présenter autrement : considérons le classifieur $f$ tel que pour tout exemple $x$
$$f(x) = (1-g(x), g(x))$$$f$ nous donne un vecteur à deux coordonnées, $f_0(x)$ et $f_1(x)$, qui sont respectivement les vraisemblances des classes $0$ et $1$.
Pour un problème à $n$ classes, on va vouloir une vraisemblance par classe, on va donc procéder de la façon suivante :
On considère des poids $(w_1, b_1), …, (w_n, b_n)$. Ils définissent un classifieur linéaire.
En effet, si on considère les $z_i$ définis pour tout exemple $x$ par
$$ \begin{cases} z_1 = w_1⋅x + b_1\\ \vdots\\ z_n = w_n⋅x + b_1 \end{cases} $$On peut choisir la classe $y$ à affecter à $x$ en prenant $y=\operatorname{argmax}\limits_i z_i$
Il reste à normaliser pour avoir des vraisemblances. Pour ça on utilise une fonction très importante : la fonction $\operatorname{softmax}$, définie ainsi :
$$\operatorname{softmax}(z_1, …, z_n) = \left(\frac{e^{z_1}}{\sum_i e^{z_i}}, …, \frac{e^{z_n}}{\sum_i e^{z_i}}\right)$$Contrairement à la fonction logistique qui prenait un nombre en entrée et renvoyait un nombre, $\operatorname{softmax}$ prend en entrée un vecteur non-normalisé et renvoie un vecteur normalisé.
On définit enfin le classifieur logistique multinomial $f$ de la façon suivante : pour tout exemple $x$, on a
$$f(x) = \operatorname{softmax}(w_1⋅x+b_1, …, w_n⋅x+b_n) = \left(\frac{e^{w_1⋅x+b_1}}{\sum_i e^{w_i⋅x+b_i}}, …, \frac{e^{w_n⋅x+b_n}}{\sum_i e^{w_i⋅x+b_i}}\right)$$et on choisit pour $x$ la classe
$$y = \operatorname{argmax}\limits_i f_i(x) = \operatorname{argmax}\limits_i \frac{e^{w_i⋅x+b_i}}{\sum_j e^{w_j⋅x+b_j}}$$Comme la fonction exponentielle est croissante, ce sera la même classe que le classifieur linéaire précédent. Comme pour le cas à deux classe, la différence se fera lors de l'apprentissage. Je vous laisse aller en lire les détails dans Speech and Language Processing, mais l'idée est la même : on utilise la log-vraisemblance négative de la classe correcte comme fonction de coût, et on optimise les paramètres avec l'algo de descente de gradient stochastique.
Un dernier détail ?
Qu'est-ce qui se passe si on prend ce qu'on vient de voir pour $n=2$ ? Est-ce qu'on retombe sur le cas à deux classe vu précédemment ?
Oui, regarde : dans ce cas
$$ \begin{align} f_1(x) &= \frac{e^{w_1⋅x+b_1}}{e^{w_0⋅x+b_0}+e^{w_1⋅x+b_1}}\\ &= \frac{1}{ \frac{e^{w_0⋅x+b_0}}{e^{w_1⋅x+b_1}} + 1 }\\ &= \frac{1}{e^{(w_0⋅x+b_0)-(w_1⋅x+b_1)} + 1}\\ &= \frac{1}{1 + e^{(w_0-w_1)⋅x+(b_0-b_1)}}\\ &= σ((w_0-w_1)⋅x+(b_0-b_1)) \end{align} $$Autrement dit, appliquer ce qu'on vient de voir pour le cas multinomial, si $n=2$, c'est comme appliquer ce qu'on a vu pour deux classes, avec $w=w_0-w_1$ et $b=b_0-b_1$.
Vous êtes arrivé⋅e⋅s au bout de ce cours et vous devriez avoir quelques idées de plusieurs concepts importants :
On reparlera de tout ça en temps utile. Pour la suite de vos aventures au pays des classifieurs logistiques, je vous recommande plutôt d'utiliser leur implémentation dans scikit-learn. Maintenant que vous savez comment ça marche, vous pouvez le faire la tête haute. Bravo !
Vous avez aussi découvert les premiers réseaux de neurones de ce cours et ce n'est pas rien !